import fs from 'fs-extra';
import { glob } from 'glob';
import { getOptions } from '../../lib/config/options';
import { packageCacheNamespaces } from '../../lib/util/cache/package/namespaces';
import { regEx } from '../../lib/util/regex';
import { templateHelperNames } from '../../lib/util/template';

const options = getOptions();
const markdownGlob = '{docs,lib}/**/*.md';

describe('docs/documentation', () => {
  it('has no invalid links', async () => {
    const markdownFiles = await glob(markdownGlob);

    await Promise.all(
      markdownFiles.map(async (markdownFile) => {
        const markdownText = await fs.readFile(markdownFile, 'utf8');
        expect(markdownText).not.toMatch(regEx(/\.md\/#/));
      }),
    );
  });

  describe('website-documentation', () => {
    async function getConfigOptionSubHeaders(
      file: string,
      configOption: string,
    ): Promise<string[]> {
      const subHeadings = [];
      const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
      const reg = regEx(`##\\s${configOption}[\\s\\S]+?\n##\\s`);
      const match = reg.exec(content);
      const subHeadersMatch = match?.[0]?.matchAll(/\n###\s(?<child>\w+)\n/g);
      if (subHeadersMatch) {
        for (const subHeaderStr of subHeadersMatch) {
          if (subHeaderStr?.groups?.child) {
            subHeadings.push(subHeaderStr.groups.child);
          }
        }
      }
      return subHeadings;
    }

    describe('docs/usage/configuration-options.md', () => {
      async function getConfigHeaders(file: string): Promise<string[]> {
        const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
        const matches = content.match(/\n## (.*?)\n/g) ?? [];
        return matches
          .map((match) => match.substring(4, match.length - 1))
          .filter(
            (header) =>
              header !== 'managerFilePatterns' && header !== 'enabled',
          );
      }

      function getRequiredConfigOptions(): string[] {
        return options
          .filter((option) => !option.globalOnly)
          .filter((option) => !option.parents)
          .filter((option) => !option.autogenerated)
          .map((option) => option.name)
          .sort();
      }

      it('has doc headers sorted alphabetically', async () => {
        expect(await getConfigHeaders('configuration-options.md')).toEqual(
          (await getConfigHeaders('configuration-options.md')).sort(),
        );
      });

      it('has headers for every required option', async () => {
        expect(await getConfigHeaders('configuration-options.md')).toEqual(
          getRequiredConfigOptions(),
        );
      });

      async function getConfigSubHeaders(file: string): Promise<string[]> {
        const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
        const matches = content.match(/\n### (.*?)\n/g) ?? [];
        return matches
          .map((match) => match.substring(5, match.length - 1))
          .sort();
      }

      function getRequiredConfigSubOptions(): string[] {
        return options
          .filter((option) => option.stage !== 'global')
          .filter((option) => !option.globalOnly)
          .filter((option) => option.parents)
          .map((option) => option.name)
          .filter(
            (header) =>
              header !== 'managerFilePatterns' && header !== 'enabled',
          )
          .sort();
      }

      function getParentNames(): Set<string> {
        const childrens = options
          .filter((option) => option.stage !== 'global')
          .filter((option) => !option.globalOnly)
          .filter((option) => option.parents);

        const parentNames = new Set<string>();
        for (const children of childrens) {
          const parents = children.parents ?? [];
          for (const parent of parents) {
            parentNames.add(parent);
          }
        }

        return parentNames;
      }

      it('has headers for every required sub-option', async () => {
        expect(await getConfigSubHeaders('configuration-options.md')).toEqual(
          getRequiredConfigSubOptions(),
        );
      });

      it.each([...getParentNames()])(
        '%s has sub-headers sorted alphabetically',
        async (parentName: string) => {
          expect(
            await getConfigOptionSubHeaders(
              'configuration-options.md',
              parentName,
            ),
          ).toEqual(
            (
              await getConfigOptionSubHeaders(
                'configuration-options.md',
                parentName,
              )
            ).sort(),
          );
        },
      );
    });

    describe('docs/usage/self-hosted-configuration.md', () => {
      async function getSelfHostedHeaders(file: string): Promise<string[]> {
        const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
        const matches = content.match(/\n## (.*?)\n/g) ?? [];
        return matches.map((match) => match.substring(4, match.length - 1));
      }

      function getRequiredSelfHostedOptions(): string[] {
        return options
          .filter((option) => option.globalOnly)
          .map((option) => option.name)
          .sort();
      }

      it('has headers sorted alphabetically', async () => {
        expect(
          await getSelfHostedHeaders('self-hosted-configuration.md'),
        ).toEqual(
          (await getSelfHostedHeaders('self-hosted-configuration.md')).sort(),
        );
      });

      it('has headers for every required option', async () => {
        expect(
          await getSelfHostedHeaders('self-hosted-configuration.md'),
        ).toEqual(getRequiredSelfHostedOptions());
      });

      function getCacheNamespacesFromDocs(file: string): string[] {
        const content = fs.readFileSync(`docs/usage/${file}`, 'utf8');
        const beginMarker = '<!-- cache-namespaces-begin -->';
        const endMarker = '<!-- cache-namespaces-end -->';

        const beginIndex = content.indexOf(beginMarker);
        const endIndex = content.indexOf(endMarker);

        if (beginIndex === -1 || endIndex === -1) {
          return [];
        }

        const cacheNamespacesSection = content.substring(
          beginIndex + beginMarker.length,
          endIndex,
        );
        const matches = cacheNamespacesSection.match(/- `([^`]+)`/g) ?? [];

        return matches
          .map((match) => match.substring(3, match.length - 1))
          .sort();
      }

      function getExpectedCacheNamespaces(): string[] {
        return packageCacheNamespaces
          .filter((namespace) => namespace !== '_test-namespace')
          .sort();
      }

      it('has complete cache namespaces list', () => {
        expect(
          getCacheNamespacesFromDocs('self-hosted-configuration.md'),
        ).toEqual(getExpectedCacheNamespaces());
      });
    });

    describe('docs/usage/self-hosted-experimental.md', () => {
      async function getSelfHostedExperimentalConfigHeaders(
        file: string,
      ): Promise<string[]> {
        const content = await fs.readFile(`docs/usage/${file}`, 'utf8');
        const matches = content.match(/\n## (.*?)\n/g) ?? [];
        return matches.map((match) => match.substring(4, match.length - 1));
      }

      it('has headers sorted alphabetically', async () => {
        expect(
          await getSelfHostedExperimentalConfigHeaders(
            'self-hosted-experimental.md',
          ),
        ).toEqual(
          (
            await getSelfHostedExperimentalConfigHeaders(
              'self-hosted-experimental.md',
            )
          ).sort(),
        );
      });
    });

    describe('docs/usage/templates.md', async () => {
      async function getAdditionalHandlebarsHelpersHeaders(): Promise<
        string[]
      > {
        const content = await fs.readFile(`docs/usage/templates.md`, 'utf8');
        const matches = content.match(/\n### (.*?)\n/g) ?? [];
        return matches.map((match) => match.substring(5, match.length - 1));
      }

      const additionalHandlebarsHelpers =
        await getAdditionalHandlebarsHelpersHeaders();

      it('has headers sorted alphabetically', () => {
        expect(additionalHandlebarsHelpers).toEqual(
          additionalHandlebarsHelpers.toSorted(),
        );
      });

      it('documents all added Handlebars helpers', () => {
        expect(additionalHandlebarsHelpers).toEqual(
          templateHelperNames.toSorted(),
        );
      });
    });
  });
});
